﻿using System;
using System.Text;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace ToyundaPhone.FileFormat
{
	abstract public class GenericDelimitedFileLexer : ILexer<DelimitedFileTokenType>
	{
		private readonly char[] delimiter;
		private readonly char? quoteChar;
		private readonly char? escapeChar;
		private readonly char[] lineEndChars;
		private readonly TextReader source;

		protected GenericDelimitedFileLexer(TextReader source, char[] delimiter, char? quoteChar, char? escapeChar, char[] lineEndChars)
		{
			if (source == null) throw new ArgumentNullException("No source specified.", "source");
			if (delimiter.Length == 0) throw new ArgumentException("Empty delimiter are not supported.", "delimiter");
			if (lineEndChars.Length == 0) throw new ArgumentException("At least one EOL character must be provided.", "lineEndChars");
			if ((delimiter.Any(c1 => lineEndChars.Any(c2 => c1 == c2))) ||
				(quoteChar.HasValue && (delimiter.Any(c => c == quoteChar.Value) || lineEndChars.Any(c => c == quoteChar.Value))) ||
				(escapeChar.HasValue && (delimiter.Any(c => c == escapeChar.Value) || lineEndChars.Any(c => c == escapeChar.Value)) ||
				(quoteChar.HasValue && escapeChar.HasValue && (quoteChar.Value == escapeChar.Value))))
				throw new ArgumentException("delimiter, quoteChar, escapeChar and lineEndChars cannot share a character.");
			this.delimiter = delimiter;
			this.quoteChar = quoteChar;
			this.escapeChar = escapeChar;
			this.lineEndChars = lineEndChars;
			this.source = source;
			this.tokenBuffer = new StringBuilder();
			this.availableTokens = new List<Token<DelimitedFileTokenType>>();
			this.sourceBuffer = new char[Math.Max(delimiter.Length * 2, 512)];
			this.sourceBufferPosition = 0;
			this.sourceBufferLength = 0;
			this.column = 1;
			this.line = 1;
			this.lastCharIsEol = false;
		}

		public char[] Delimiter
		{
			get { return delimiter; }
		}

		public char? QuoteChar
		{
			get { return quoteChar; }
		}

		public char? EscapeChar
		{
			get { return escapeChar; }
		}

		public char[] LineEndChars
		{
			get { return lineEndChars; }
		}

		private StringBuilder tokenBuffer;
		private List<Token<DelimitedFileTokenType>> availableTokens;
		private char[] sourceBuffer;
		private int sourceBufferPosition;
		private int sourceBufferLength;
		private int column;
		private int line;
		private bool lastCharIsEol;

		private bool ReadSource()
		{
			int start = 0;
			if (sourceBufferPosition < sourceBufferLength)
			{
				start = sourceBufferLength - sourceBufferPosition;
				for (int i = 0; i < start; i++)
				{
					sourceBuffer[i] = sourceBuffer[sourceBufferPosition + i];
				}
			}
			int read = source.Read(sourceBuffer, start, sourceBuffer.Length - start);
			sourceBufferPosition = 0;
			if (read > 0)
			{
				sourceBufferLength = read + start;
			}
			else
			{
				sourceBufferLength = start;
			}
			return read > 0;
		}

		private char? ReadChar(int aheadPosition = 0)
		{
			bool @continue = true;
			char? result = null;
			if (sourceBufferLength >= 0)
			{
				if ((sourceBufferPosition + aheadPosition) >= sourceBufferLength)
				{
					@continue = ReadSource();
				}
				if (@continue)
				{
					result = sourceBuffer[sourceBufferPosition + aheadPosition];
				}
			}
			return result;
		}

		private void ConsumeChar()
		{
			if (lineEndChars.Any(c => c == sourceBuffer[sourceBufferPosition]))
			{
				if (!lastCharIsEol)
				{
					column = 1;
					line++;
				}
			}
			else
			{
				lastCharIsEol = false;
				column++;
			}
			sourceBufferPosition++;
		}

		private bool IsDelimiter()
		{
			bool result = true;
			for (int i = 0; i < delimiter.Length; i++)
			{
				char? current = ReadChar(i);
				if (!current.HasValue || (current.Value != delimiter[i]))
				{
					result = false;
					break;
				}
			}
			return result;
		}

		private bool ReadToken()
		{
			bool result = false;
			char? current = ReadChar();
			if (current.HasValue)
			{
				if (lineEndChars.Any(c => current.Value == c))
				{
					result = ReadEOLToken();
				}
				else if (quoteChar.HasValue && (current.Value == quoteChar.Value))
				{
					result = ReadQuotedColumnToken();
				}
				else if ((current.Value == delimiter[0]) && IsDelimiter())
				{
					result = ReadDelimiterToken();
				}
				else
				{
					result = ReadColumnToken();
				}
			}
			else
			{
				if (sourceBufferPosition >= 0)
				{
					result = ReadEOFtoken();
					sourceBufferPosition = -1;
				}
			}
			return result;
		}

		private bool ReadEOFtoken()
		{
			availableTokens.Add(new Token<DelimitedFileTokenType>(DelimitedFileTokenType.EOF, string.Empty, line, column));
			return true;
		}

		private bool ReadEOLToken()
		{
			int column = this.column;
			int line = this.line;
			char? current = ReadChar();
			while (current.HasValue && lineEndChars.Any(c => c == current.Value))
			{
				tokenBuffer.Append(current.Value);
				ConsumeChar();
				current = ReadChar();
			}
			if (tokenBuffer.Length == 0) throw new InvalidOperationException("An EOL token cannot be empty. No line ending found.");
			availableTokens.Add(new Token<DelimitedFileTokenType>(DelimitedFileTokenType.EOL, tokenBuffer.ToString(), line, column));
			tokenBuffer.Clear();
			return true;
		}

		private bool ReadQuotedColumnToken()
		{
			int column = this.column;
			int line = this.line;
			char? current = ReadChar();
			if (!quoteChar.HasValue) throw new NotSupportedException("Could not read a quoted column when there is no quote char.");
			if (!current.HasValue || (current.Value != quoteChar.Value))
			{
				throw new InvalidOperationException("Trying to read a QuotedColumn with no starting quote character.");
			}
			ConsumeChar();
			current = ReadChar();
			while (current.HasValue && (current.Value != quoteChar.Value))
			{
				if (escapeChar.HasValue && (current.Value == escapeChar.Value))
				{
					ConsumeChar();
					current = ReadChar();
					if (!current.HasValue)
					{
						throw new InvalidOperationException("EOF when reading an escaped char of a quoted column.");
					}
				}
				tokenBuffer.Append(current.Value);
				ConsumeChar();
				current = ReadChar();
			}
			if (!current.HasValue)
			{
				throw new InvalidOperationException("EOF when reading a quoted column.");
			}
			ConsumeChar();
			availableTokens.Add(new Token<DelimitedFileTokenType>(DelimitedFileTokenType.QuotedColumn, tokenBuffer.ToString(), line, column));
			tokenBuffer.Clear();
			return true;
		}

		private bool ReadColumnToken()
		{
			int column = this.column;
			int line = this.line;
			char? current = ReadChar();
			while (current.HasValue && (!lineEndChars.Any(c => c == current.Value) && (current.Value != delimiter[0] || !IsDelimiter())))
			{
				tokenBuffer.Append(current.Value);
				ConsumeChar();
				current = ReadChar();
			}
			availableTokens.Add(new Token<DelimitedFileTokenType>(DelimitedFileTokenType.Column, tokenBuffer.ToString(), line, column));
			tokenBuffer.Clear();
			return true;
		}

		private bool ReadDelimiterToken()
		{
			int column = this.column;
			int line = this.line;
			char? current = ReadChar();
			int pos = 0;
			while ((pos < delimiter.Length) && current.HasValue && (current.Value == delimiter[pos]))
			{
				tokenBuffer.Append(current.Value);
				ConsumeChar();
				current = ReadChar();
				pos++;
			}
			if (tokenBuffer.Length != delimiter.Length) throw new InvalidOperationException("A delimiter could not be completly read.");
			availableTokens.Add(new Token<DelimitedFileTokenType>(DelimitedFileTokenType.Delimiter, tokenBuffer.ToString(), line, column));
			tokenBuffer.Clear();
			return true;
		}

		public Token<DelimitedFileTokenType> GetToken(int ahead = 0)
		{
			while (availableTokens.Count <= ahead)
			{
				if (!ReadToken())
				{
					break;
				}
			}
			Token<DelimitedFileTokenType> token = null;
			if (availableTokens.Count > ahead)
			{
				token = availableTokens[ahead];
			}
			return token;
		}

		public void ConsumeTokens(int count = 1)
		{
			GetToken(count - 1);
			availableTokens.RemoveRange(0, count);
		}
	}
}
